package org.etnaframework.module.cron;

import org.etnaframework.module.base.config.IgnoredPackages;
import org.etnaframework.module.base.spring.SpringContext;
import org.etnaframework.module.base.utils.CronExecutorUtils;
import org.etnaframework.module.base.utils.DatetimeUtils;
import org.etnaframework.module.base.utils.NamedThreadFactory;
import org.etnaframework.module.base.utils.ReflectUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.boot.CommandLineRunner;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;

/**
 * 定时任务初始化处理器
 *
 * @author jasyaf
 * @since 2024-04-06
 */
public class CronInitializer implements CommandLineRunner {

    static final Logger log = LoggerFactory.getLogger(CronInitializer.class);

    private static final Set<CronTask> cronTasks = new LinkedHashSet<>();

    static final ScheduledExecutorService cronDispatcher = Executors.newScheduledThreadPool(Runtime.getRuntime().availableProcessors(), new NamedThreadFactory("DispatchCron::", Thread.MAX_PRIORITY, true));

    static final ExecutorService cronExecutor = Executors.newCachedThreadPool(new NamedThreadFactory("Cron::", Thread.NORM_PRIORITY, true));

    static final String TASK_CHECKER_CRON = "*/1 * * * * *";

    static volatile boolean SHUTDOWN_FLAG = false;

    public static Set<CronTask> getCronTasks() {
        return cronTasks;
    }

    private final ICronCustomizer crontabCustomizer;

    public CronInitializer(ICronCustomizer crontabCustomizer) {
        this.crontabCustomizer = crontabCustomizer;
    }

    /**
     * 列举所有的Spring托管bean，将所有标注了{@link Crontab}的方法列举出来，初始化定时任务
     */
    @Override
    public void run(String... args) throws Exception {
        if (crontabCustomizer.enabled()) {
            long start = System.currentTimeMillis();
            int count = 0;

            Runtime.getRuntime().addShutdownHook(new Thread(() -> {
                log.info("Stop @{} tasks", Crontab.class.getSimpleName());
                SHUTDOWN_FLAG = true;
            }));

            for (Object bean : SpringContext.getBeansOfTypeAsList(Object.class)) {
                if (IgnoredPackages.filter(bean)) {
                    continue;
                }
                for (Method m : ReflectUtils.getAllMethodsInSourceCodeOrder(bean.getClass())) {
                    Crontab c = m.getAnnotation(Crontab.class);
                    if (null != c) {
                        String errorMessage = "Method of class " + bean.getClass().getName() + " with @" + Crontab.class.getSimpleName() + ": " + m.getName();
                        // 防止跟AOP混用时，由于生成类不包含私有方法导致出错，故限制
                        if (Modifier.isPrivate(m.getModifiers())) {
                            throw new IllegalArgumentException(errorMessage + "CANNOT be private");
                        }
                        // 写在基类中的static方法，在继承类中会被重复调用造成混乱，故限制
                        if (Modifier.isStatic(m.getModifiers())) {
                            throw new IllegalArgumentException(errorMessage + "CANNOT be static");
                        }
                        // 定时调用的方法不能带参数，也没有地方可以供传参，故限制
                        if (m.getParameterCount() > 0) {
                            throw new IllegalArgumentException(errorMessage + "CANNOT have arguments");
                        }
                        if (CronRunModeEnum.ChampionOnly.equals(c.runMode()) && null == crontabCustomizer.getRedisConnectionFactory()) {
                            throw new IllegalArgumentException(errorMessage + " defines runMode=" + CronRunModeEnum.ChampionOnly + " but " + crontabCustomizer.getClass().getSimpleName() + ".getRedisConnectionFactory() CANNOT return null!!");
                        }
                        CronTask task = new CronTask(crontabCustomizer, c.cron(), c.runMode(), c.timeoutSec() * DatetimeUtils.MILLIS_PER_SECOND, bean, m);
                        if (cronTasks.add(task)) {
                            CronExecutorUtils.schedule(cronDispatcher, cronExecutor, c.cron(), task);
                            log.info("Add @{} Task: cron={}, runMode={}, timeout={}s, location={}", Crontab.class.getSimpleName(), task.getCron(), task.getRunMode(), c.timeoutSec(), task.getLocation());
                        } else {
                            throw new IllegalArgumentException("Duplicated crontab task: " + task.getLocation());
                        }
                        count++;
                    }
                }
            }
            // 【定时任务超时扫描】任务
            CronExecutorUtils.schedule(cronDispatcher, cronExecutor, TASK_CHECKER_CRON, () -> {
                for (CronTask ct : getCronTasks()) {
                    if (ct.timeoutMs > 0) {
                        long now = System.currentTimeMillis();
                        if (now > ct.nextTimeoutReportTimeMs) { // 执行时间超过 timeoutMs * n 时触发告警
                            ct.nextTimeoutReportTimeMs = now + ct.timeoutMs;
                            if (ct.running) {
                                long runningMs = now - ct.lastStartTime;
                                if (runningMs > ct.timeoutMs) {
                                    CronTask.log.error("Run {}-Task Timeout: location={}, startAt={}, runningMs={}ms", ct.runMode, ct.location, DatetimeUtils.format(ct.lastStartTime), runningMs);
                                    Map<String, String> mdc = MDC.getCopyOfContextMap();
                                    cronExecutor.execute(() -> {
                                        try {
                                            MDC.setContextMap(mdc);
                                            crontabCustomizer.onTimeout(ct.location, ct.lastStartTime);
                                        } finally {
                                            MDC.clear();
                                        }
                                    });
                                }
                            }
                        }
                    }
                    cronExecutor.execute(ct::renewLockIfObtained);
                }
            });
            log.info("Module @{} Enabled({}) [{}ms]", Crontab.class.getSimpleName(), count, System.currentTimeMillis() - start);
        } else {
            log.info("Module @{} Disabled", Crontab.class.getSimpleName());
        }
    }
}
